
package org.apache.solr.core;

import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.io.IOUtils;
import org.apache.lucene.util.Version;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.XMLErrorLogger;
import org.apache.solr.util.DOMUtil;
import org.apache.solr.util.SystemIdResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class Config {

    public static final Logger log = LoggerFactory.getLogger(Config.class);
    private static final XMLErrorLogger xmllog = new XMLErrorLogger(log);
    static final XPathFactory xpathFactory = XPathFactory.newInstance();
    private final Document doc;
    private final String prefix;
    private final String name;
    private final SolrResourceLoader loader;

    /**
     * Builds a config from a resource name with no xpath prefix.
     */
    public Config(SolrResourceLoader loader, String name) throws ParserConfigurationException, IOException, SAXException {
        this(loader, name, null, null);
    }

    public Config(SolrResourceLoader loader, String name, InputSource is, String prefix) throws ParserConfigurationException, IOException, SAXException {
        this(loader, name, is, prefix, true);
    }

    /**
     * Builds a config: <p> Note that the 'name' parameter is used to obtain a
     * valid input stream if no valid one is provided through 'is'. If no valid
     * stream is provided, a valid SolrResourceLoader instance should be
     * provided through 'loader' so the resource can be opened (
     *
     * @see SolrResourceLoader#openResource); if no SolrResourceLoader instance
     * is provided, a default one will be created. </p> <p> Consider passing a
     * non-null 'name' parameter in all use-cases since it is used for logging &
     * exception reporting. </p>
     * @param loader the resource loader used to obtain an input stream if 'is'
     * is null
     * @param name the resource name used if the input stream 'is' is null
     * @param is the resource as a SAX InputSource
     * @param prefix an optional prefix that will be preprended to all
     * non-absolute xpath expressions
     */
    public Config(SolrResourceLoader loader, String name, InputSource is, String prefix, boolean subProps) throws ParserConfigurationException, IOException, SAXException {

        if (loader == null) {
            loader = new SolrResourceLoader(null);
        }
        this.loader = loader;
        this.name = name;
        this.prefix = (prefix != null && !prefix.endsWith("/")) ? prefix + '/' : prefix;
        try {
            javax.xml.parsers.DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

            if (is == null) {
                is = new InputSource(loader.openConfig(name));
                is.setSystemId(SystemIdResolver.createSystemIdFromResourceName(name));
            }

            // only enable xinclude, if a SystemId is available
            if (is.getSystemId() != null) {
                try {
                    dbf.setXIncludeAware(true);
                    dbf.setNamespaceAware(true);
                }
                catch (UnsupportedOperationException e) {
                    log.warn(name + " XML parser doesn't support XInclude option");
                }
            }

            final DocumentBuilder db = dbf.newDocumentBuilder();
            db.setEntityResolver(new SystemIdResolver(loader));
            db.setErrorHandler(xmllog);
            try {
                doc = db.parse(is);
            }
            finally {
                // some XML parsers are broken and don't close the byte stream (but they should according to spec)
                IOUtils.closeQuietly(is.getByteStream());
            }
            if (subProps) {
                DOMUtil.substituteProperties(doc, loader.getCoreProperties());
            }
        }
        catch (ParserConfigurationException | SAXException | SolrException e) {
            SolrException.log(log, "Exception during parsing file: " + name, e);
            throw e;
        }
    }

    public Config(SolrResourceLoader loader, String name, Document doc) {

        this.prefix = null;
        this.doc = doc;
        this.name = name;
        this.loader = loader;
    }

    /**
     * @since solr 1.3
     */
    public SolrResourceLoader getResourceLoader() {
        return loader;
    }

    /**
     * @since solr 1.3
     */
    public String getResourceName() {
        return name;
    }

    public String getName() {
        return name;
    }

    public Document getDocument() {
        return doc;
    }

    public XPath getXPath() {
        return xpathFactory.newXPath();
    }

    private String normalize(String path) {
        return (prefix == null || path.startsWith("/")) ? path : prefix + path;
    }

    public void substituteProperties() {
        DOMUtil.substituteProperties(doc, loader.getCoreProperties());
    }

    public Object evaluate(String path, QName type) {
        XPath xpath = xpathFactory.newXPath();
        try {
            String xstr = normalize(path);

            // TODO: instead of prepending /prefix/, we could do the search rooted at /prefix...
            Object o = xpath.evaluate(xstr, doc, type);
            return o;
        }
        catch (XPathExpressionException e) {
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in xpath:" + path + " for " + name, e);
        }
    }

    public Node getNode(String path, boolean errIfMissing) {

        XPath xpath = xpathFactory.newXPath();
        String xstr = normalize(path);

        try {
            Node nd = (Node) xpath.evaluate(xstr, doc, XPathConstants.NODE);

            if (nd == null) {
                if (errIfMissing) {
                    throw new RuntimeException(name + " missing " + path);
                }
                else {
                    log.debug(name + " missing optional " + path);
                    return null;
                }
            }

            log.trace(name + ":" + path + "=" + nd);
            return nd;

        }
        catch (XPathExpressionException e) {
            SolrException.log(log, "Error in xpath", e);
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in xpath:" + xstr + " for " + name, e);
        }
        catch (SolrException e) {
            throw (e);
        }
        catch (Throwable e) {
            SolrException.log(log, "Error in xpath", e);
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in xpath:" + xstr + " for " + name, e);
        }
    }

    public String getVal(String path, boolean errIfMissing) {

        Node nd = getNode(path, errIfMissing);
        if (nd == null) {
            return null;
        }

        String txt = DOMUtil.getText(nd);

        log.debug(name + ' ' + path + '=' + txt);
        return txt;

        /**
         * short typ = nd.getNodeType(); if (typ==Node.ATTRIBUTE_NODE ||
         * typ==Node.TEXT_NODE) { return nd.getNodeValue(); } return
         * nd.getTextContent();
         */
    }

    public String get(String path) {
        return getVal(path, true);
    }

    public String get(String path, String def) {
        String val = getVal(path, false);
        if (val == null || val.length() == 0) {
            return def;
        }
        return val;
    }

    public int getInt(String path) {
        return Integer.parseInt(getVal(path, true));
    }

    public int getInt(String path, int def) {
        String val = getVal(path, false);
        return val != null ? Integer.parseInt(val) : def;
    }

    public boolean getBool(String path) {
        return Boolean.parseBoolean(getVal(path, true));
    }

    public boolean getBool(String path, boolean def) {
        String val = getVal(path, false);
        return val != null ? Boolean.parseBoolean(val) : def;
    }

    public float getFloat(String path) {
        return Float.parseFloat(getVal(path, true));
    }

    public float getFloat(String path, float def) {
        String val = getVal(path, false);
        return val != null ? Float.parseFloat(val) : def;
    }

    public double getDouble(String path) {
        return Double.parseDouble(getVal(path, true));
    }

    public double getDouble(String path, double def) {
        String val = getVal(path, false);
        return val != null ? Double.parseDouble(val) : def;
    }

    public Version getLuceneVersion(String path) {
        return parseLuceneVersionString(getVal(path, true));
    }

    public Version getLuceneVersion(String path, Version def) {
        String val = getVal(path, false);
        return val != null ? parseLuceneVersionString(val) : def;
    }
    private static final AtomicBoolean versionWarningAlreadyLogged = new AtomicBoolean(false);

    public static Version parseLuceneVersionString(final String matchVersion) {
        final Version version;
        try {
            version = Version.parseLeniently(matchVersion);
        }
        catch (IllegalArgumentException iae) {
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
                    "Invalid luceneMatchVersion '" + matchVersion
                    + "', valid values are: " + Arrays.toString(Version.values())
                    + " or a string in format 'V.V'", iae);
        }

        if (version == Version.LUCENE_CURRENT && !versionWarningAlreadyLogged.getAndSet(true)) {
            log.warn(
                    "You should not use LUCENE_CURRENT as luceneMatchVersion property: "
                    + "if you use this setting, and then Solr upgrades to a newer release of Lucene, "
                    + "sizable changes may happen. If precise back compatibility is important "
                    + "then you should instead explicitly specify an actual Lucene version.");
        }

        return version;
    }
}
